use std::{cmp::Ordering, ffi::OsStr, path::Path};

use markdown::mdast::Node;
use tera::{Number, Value};
use tracing::{debug, error};
use crate::{EdgeError, Result};


struct MdLoader {
    opt: markdown::Options
}

impl MdLoader {
    fn new() -> Self {
        let mut opt = markdown::Options::default();
        opt.parse.constructs.frontmatter = true;
        Self { opt }
    }

    fn load(&self, path: &std::path::Path) -> Result<Value> {
        use std::fs::*;
        let md = read_to_string(path)?;
        let mdtree = markdown::to_mdast(&md, &self.opt.parse)
            .map_err(|s| EdgeError::MdParse(s))?;
        let frontmatter = extract_toml_frontmatter(&mdtree)
            .ok_or_else(|| EdgeError::NoFrontMatter(path.to_string_lossy().to_string()))?;
        let mut frontmatter: Value = toml::from_str(frontmatter)?;
        let html = markdown::to_html_with_options(&md, &self.opt)
            .map_err(|s| EdgeError::MdParse(s))?;  
        if let Value::Object(o) = &mut frontmatter {
            o.insert("content_html".to_owned(), Value::String(html));
        } else {
            error!(message="Front matter isn't an object, so cannot insert content", path=?path);
        }
        Ok(frontmatter)
    }
}


pub fn load_markdown(dir_path: &Path, sort_key: String) -> Result<Vec<Value>> {
    use std::fs::*;
    let md_ext = OsStr::new("md");
    let mut rv = vec![];

    let loader = MdLoader::new();

    for de in read_dir(dir_path)? {
        let de = de?;
        let ft = de.file_type()?;
        if !(ft.is_file() || ft.is_symlink()) { continue; }
        let path = de.path();
        if path.extension() != Some(&md_ext) { continue; }
        debug!(event="start_load_markdown_file", file=?de);
        match loader.load(&path) {
            Ok(value) => rv.push(value),
            Err(e) => error!(message="error processing md file(ignored)", path=?path, error=?e),
        }
    }

    rv.sort_by(|w1, w2| compare_jsvalues(&w1[&sort_key], &w2[&sort_key]));

    Ok(rv)
}


pub fn compare_jsnumbers(v1: &Number, v2: &Number) -> Ordering {
    match ((v1.as_i64(), v1.as_u64(), v1.as_f64()), (v2.as_i64(), v2.as_u64(), v2.as_f64())) {
        
        ((Some(w1), _, _), (Some(w2), _, _)) => w1.cmp(&w2),
        ((_, Some(w1), _), (_, Some(w2), _)) => w1.cmp(&w2),
        ((_, _, Some(w1)), (_, _, Some(w2))) => if w1 < w2 { Ordering::Less } else { Ordering::Greater },
        ((Some(w1), _, _), (_, Some(w2), _)) => w1.cmp(&(w2 as i64)),
        ((Some(w1), _, _), (_, _, Some(w2))) => w1.cmp(&(w2 as i64)),
        ((_, Some(w1), _), (Some(w2), _, _)) => w1.cmp(&(w2 as u64)),
        ((_, Some(w1), _), (_, _, Some(w2))) => w1.cmp(&(w2 as u64)),
        ((_, _, Some(w1)), (Some(w2), _, _)) => (w1 as i64).cmp(&w2),
        ((_, _, Some(w1)), (_, Some(w2), _)) => (w1 as u64).cmp(&w2),
        _ => Ordering::Equal,
    }    
}

pub fn t_idx(value: &Value) -> u8 {
    match value {
        Value::Null => 0,
        Value::Bool(_) => 1,
        Value::Number(_) => 2,
        Value::String(_) => 3,
        Value::Array(_) => 4,
        Value::Object(_) => 5,        
    }
}

pub fn compare_jsvalues(v1: &Value, v2: &Value) -> Ordering {
    match (v1, v2) {
        (Value::Null, Value::Null) => Ordering::Equal,
        (Value::Bool(b1), Value::Bool(b2)) => b1.cmp(b2),
        (Value::String(s1), Value::String(s2)) => s1.cmp(s2),
        (Value::Number(n1), Value::Number(n2)) => compare_jsnumbers(n1, n2),
        (a, b) => t_idx(a).cmp(&t_idx(b)),
    }
}

/*
pub fn get_property_value<'t>(w: &'t Value, k: &str) -> Option<&'t Value> {
    let Value::Object(o) = w else { return None };
    o.get(k)
}

pub fn get_property_value_or_null<'t>(w: &'t Value, k: &str) -> Cow<'t, Value> {
    match get_property_value(w, k) {
        Some(w) => Cow::Borrowed(w),
        None => Cow::Owned(Value::Null)
    }
}
*/

fn extract_toml_frontmatter(n: &markdown::mdast::Node) -> Option<&str> {
    if let Node::Root(r) = &n { if let Some(Node::Toml(t)) = &r.children.first() { return Some(&t.value) } };
    None
}


#[test]
fn test_md() {
    let md = r#"+++
abc="def"
xyz=10
+++
A markdown text having some _emphasized_ or **bold** chars.

Another para.
"#;

    let h_exp = r#"<p>A markdown text having some <em>emphasized</em> or <strong>bold</strong> chars.</p>
<p>Another para.</p>
"#;
    let t_exp = r#"abc="def"
xyz=10"#;

    let mut opt = markdown::Options::default();
    opt.parse.constructs.frontmatter = true;

    let n= markdown::to_mdast(md, &opt.parse).unwrap();
    let toml = extract_toml_frontmatter(&n).unwrap();
    assert_eq!(t_exp, toml);
    let h = markdown::to_html_with_options(md, &opt).unwrap();
    assert_eq!(h_exp, h);
}